%% @title ErlTL: A Simple Erlang template languages
%% @author Yariv Sadan (yarivsblog@gmail.com, http://yarivsblog.com)
%%
%% @doc ErlTL is a simple Erlang-based template language. It emphasizes
%%  speed, simplicity, reusability and good error reporting.
%%  For more information, visit http://code.google.com/p/erltl

-module(erltl).
-author("Yariv Sadan (yarivsblog@gmail.com, http://yarivsblog.com)").
-export([compile/1, compile/2]).

%% @doc Compile the template file with the default options. The code will be
%%  loaded (or swapped if necessary) into the VM and the .beam file
%%  will be written to the same directory as the template file.
%%
%% @spec compile(FileName::string()) -> ok | {error, Err}
compile(FileName) ->
    compile(FileName, [{outdir, filename:dirname(FileName)},
		       report_errors, report_warnings, nowarn_unused_vars]).

%% @doc Compile the template file with user defined options. For more information,
%%  visit file:///Users/yariv/Desktop/otp_doc_html_R11B-0/doc/index.html.
compile(FileName, Options) ->
    case file:read_file(FileName) of
	{ok, Binary} ->
	    Lines = make_lines(binary_to_list(Binary)),
	    BaseName = filename:rootname(filename:basename(FileName)),
	    Module = list_to_atom(BaseName),
	    case catch tree(Lines, Module) of
		{ok, Tree} ->
		    case compile:forms(Tree,
				       Options) of
			{ok, Module, Bin} ->
			    OutDir = case lists:keysearch(outdir, 1, Options) of
					 {value, {outdir, Val}} -> Val;
					 false -> filename:dirname(FileName)
				     end,
			    case file:write_file(OutDir ++ ['/' | BaseName] ++
						 ".beam", Bin) of
				ok ->
				    code:purge(Module),
				    case code:load_binary(
					   Module, atom_to_list(Module), Bin) of
					{module, _Module} ->
					    ok;
					Err ->
					    Err
				    end;
				{error, _} = Err ->
				    Err
			    end;
			Err ->
			    Err
		    end;
		{'EXIT', Err} -> {error, Err};
		Err -> Err
	    end;
	Err -> Err
    end.

make_lines(Str) ->
    make_lines(Str, [], []).

make_lines([], [], Result) -> lists:reverse(Result);
make_lines([], Acc, Result) -> lists:reverse([lists:reverse(Acc) | Result]);
make_lines([10 | Tail], Acc, Result) ->
    make_lines(Tail, [], [lists:reverse(Acc) | Result]);
make_lines([Head | Tail], Acc, Result) ->
    make_lines(Tail, [Head | Acc], Result).
    
    

tree(Lines, Module) ->
    case parse(Lines) of
	{ok, Forms} ->
	    Tree = 
	     [{attribute,1,module,Module},
	      {attribute,2,file,{atom_to_list(Module),1}},
	      {attribute,3,compile,export_all}] ++ Forms,
	    {ok, Tree};
	Err -> Err
    end.


parse(Lines) ->
    {ok, FuncDeclRe} = regexp:parse("^<%@.+%>\w*"),
    {FuncName3, LastMode, _LineNo, TopExprs3, Exprs3, _AtTop, Forms3}  = 
	lists:foldl(
	  fun(Line, {FuncName, Mode, LineNo, TopExprs, Exprs, _AtTop, Forms} = Acc) ->
		  {Line1, {FuncName1, Mode1, LineNo1, TopExprs1, Exprs1,
			   AtTop1, Forms1}} =
		      case get_new_func_name(Line, LineNo, Mode, FuncDeclRe) of
			  {name, NewFuncName, NextChars} ->
			      FuncForms = make_func_forms(FuncName,
						    TopExprs,
						    Exprs),
			      {NextChars, {NewFuncName, binary, LineNo, [], [], true,
					   FuncForms ++ Forms}};
			  none -> {Line, Acc};
			  Other -> exit(Other)
		      end,
		  {TopExprs2, Exprs2, Mode2, AtTop2} =
		      parse_chunk(Line1, Mode1, LineNo1, AtTop1),
		  {FuncName1, Mode2, LineNo1 + 1, TopExprs2 ++ TopExprs1,
		   Exprs2 ++ Exprs1, AtTop2, Forms1}
	  end, {render, binary, 1, [], [], true, []}, Lines),
    LastForms = make_func_forms(FuncName3, TopExprs3, Exprs3),
    if LastMode == erlang ->
	    {error, missing_final_code_delimeter};
       true ->
	    {ok, lists:reverse(LastForms ++  Forms3)}
    end.

get_new_func_name(Line, LineNo, Mode, FuncDeclRe) ->
   if Mode == binary ->
	    case regexp:match(Line, FuncDeclRe) of
		{match,First,Count} ->
		    FuncNameStr = lists:sublist(Line, First+3, Count-5),
		    case string:strip(FuncNameStr) of
			[] -> exit({missing_function_name, {line, LineNo}, Line});
			FuncName -> {name, list_to_atom(FuncName),
				     lists:nthtail(Count, Line)}
		    end;
		nomatch -> none
	    end;
       true -> none
    end.

make_func_forms(FuncName, TopExprs, Exprs) ->
    [{function,1,FuncName,0,
      [{clause,1,[],[],[{call,1,{atom,1,FuncName},[{atom,1,undefined}]}]}]},
     {function,1,FuncName,1,[{clause,1,[{var,1,'Data'}],[],
			      lists:reverse(TopExprs) ++
			      [cons_exprs(lists:reverse(Exprs))]}]}].


parse_chunk([], Mode, _Line, AtTop) ->
    {[],[{bin,1,[{bin_element,1,{integer,1,10},default,default}]}],Mode,AtTop};
parse_chunk(Chunk, Mode, Line, AtTop) ->
    Delim =
	case Mode of
	    binary -> "<%";
	    _ -> "%>"
	end,
    
    IsTopExprs = Mode == erlang andalso AtTop,

    case string:str(Chunk, Delim) of
	0->
	    Exprs = abstract_exprs(Chunk, {Mode, true}, Line),
	    if IsTopExprs ->
		    {Exprs, [], Mode, IsTopExprs};
	       true ->
		    {[], Exprs, Mode, IsTopExprs}
	    end;
	Pos ->
	    {First, [_,_ | Rest] = NextExpr} = lists:split(Pos-1, Chunk),
	    {ParseModeRest, Rest1, AtTop1} =
		case {Mode,Rest} of
		    {erlang,_} -> {binary, Rest, AtTop};
		    {comment,_} -> {binary, Rest, AtTop};
		    {binary, [Head | Tail]} ->
			case Head of
			    $! -> {comment, Tail, AtTop};
			    $? ->
				if AtTop ->
					{erlang, Tail, AtTop};
				   true ->
					exit({misplaced_top_expression, {line, Line},
					      NextExpr})
				end;
			    _ -> {erlang, Rest, false}
			end;
		    {binary, []} -> {erlang, Rest, false}
		end,
	    FirstExprs = abstract_exprs(First, {Mode, false}, Line),
	    {TopExprs, Exprs} =
		if IsTopExprs -> {FirstExprs, []};
		   true -> {[], FirstExprs}
		end,
	    {TopExprs1, Exprs1, FinalMode, FinalAtTop} =
		parse_chunk(Rest1, ParseModeRest, Line, AtTop1),
	    {TopExprs1 ++ TopExprs, Exprs1 ++ Exprs, FinalMode, FinalAtTop}
    end.

abstract_exprs([], _Mode, _Line) -> [];
abstract_exprs(_Chunk, {comment, _HasLineBreak}, _Line) -> [];
abstract_exprs(Chunk, {binary, HasLineBreak}, L) ->
    Chunk1 = if HasLineBreak ->
		     [10 | lists:reverse(Chunk)];
		true -> lists:reverse(Chunk)
	     end,
    BinElems =
	lists:foldl(
	  fun(Char, BinElems) ->
		  [{bin_element,L,{integer,L,Char},default,default} | BinElems]
	  end, [], Chunk1),
    
    [{bin,L,BinElems}];
abstract_exprs(Chunk, {erlang, NeedsEndl}, L) ->
    case erl_scan:string(Chunk) of
	{ok, Tokens, _} ->
	    Tokens1 =
		case {lists:reverse(Tokens), NeedsEndl} of
		    {[{',',_} | Rest], _} ->
			lists:reverse([{dot,1} | Rest]);
		    {[{dot,_} | _Rest], true} ->
			exit({expecting_comma,
			      {{line, L}, {chunk, Chunk}}});
		    {[{dot,_} | _Rest], false} ->
			exit({expecting_comma_or_no_dot,
			      {{line, L}, {chunk, Chunk}}});
		    {Other, _} ->
			if NeedsEndl ->
				      exit({missing_line_delimiter,
					    {{line, L}, {chunk, Chunk}}});
				 true  ->
				      lists:reverse([{dot,1} | Other])
			      end
		end,
	    case erl_parse:parse_exprs(Tokens1) of
		{ok, Exprs} ->
		    change_line_numbers(L, Exprs);
		{error,{_,_,Msg}} ->
		    exit({parse_error,
			  {{line, L}, {chunk, Chunk}, {msg, Msg}}})
	    end;    
	Err ->
	    exit({scan_error, {{line,L}, Chunk, Err}})
    end.

cons_exprs([]) -> {nil,1};
cons_exprs([{bin,L,BinElems}, {bin,_,BinElems1} | Rest]) ->
    cons_exprs([{bin,L,BinElems ++ BinElems1} | Rest]);
cons_exprs([Expr|Rest]) ->
    {cons,1,Expr,cons_exprs(Rest)}.
    

change_line_numbers(L, Exprs) when is_list(Exprs) ->
    lists:foldl(
      fun(Expr, Acc) ->
	      [change_line_numbers(L, Expr) | Acc]
      end, [], lists:reverse(Exprs));
change_line_numbers(L, Expr) when is_tuple(Expr) ->
    Expr1 = case is_integer(element(2, Expr)) of
		true -> setelement(2, Expr, L);
		false -> Expr
	    end,
    Elems = tuple_to_list(Expr1),
    NewElems =
	lists:foldl(
	  fun(Elem, Acc) ->
		  NewElem = change_line_numbers(L, Elem),
		  [NewElem | Acc]
	  end, [], lists:reverse(Elems)),
    list_to_tuple(NewElems);
change_line_numbers(_L, Expr) ->
    Expr.
